import { Disposable, type IFreeze, type IRange, IUniverInstanceService, type Workbook } from '@univerjs/core';
import { Inject, Injector } from '@univerjs/core';
import { IJnpfPrintService, JnpfSheetsPrintService } from './sheet-print.service';
import { SheetPrintInterceptorService, SheetSkeletonManagerService, VIEWPORT_KEY } from '@univerjs/sheets-ui';
import {
  CanvasRenderMode,
  DEFAULT_FONTFACE_PLANE,
  Engine,
  IRenderManagerService,
  Scene,
  Spreadsheet,
  SpreadsheetSkeleton,
  Viewport,
} from '@univerjs/engine-render';
import { JnpfPaperAlign, JnpfPrintPageInfo } from '../utils/define';
import { UniverType } from '@univerjs/protocol';

function roundScale(num, fix) {
  return Math.round(num * fix) / fix;
}

interface PrintConfig {
  unitId: string;
  subUnitId: string;
  scale: number;
  freeze: IFreeze;
  range: IRange;
  page: number;
}

export class JnpfSheetsPrintUiService extends Disposable {
  private _config: PrintConfig;
  private readonly _isPreview: boolean;
  private _previewScale: number;
  private _engine: Engine;
  private _scene: Scene;
  private readonly _key: string;
  private _viewMain: Viewport;
  private _viewLeft: Viewport;
  private _viewTop: Viewport;
  private _viewLeftTop: Viewport;
  private _container = document.createElement('div');
  private _jnpfPrintService: JnpfSheetsPrintService;
  private readonly _skeleton: SpreadsheetSkeleton;
  private _univerInstanceService: IUniverInstanceService;
  private _sheetPrintInterceptorService: SheetPrintInterceptorService;
  private _dirty: boolean;
  private _spreadsheetObject;
  private _totalWidth: number;
  private _totalHeight: number;

  constructor(
    @Inject(Injector) private readonly _accessor: Injector,
    config: PrintConfig = {},
    isPreview = false,
    previewScale = 1,
  ) {
    super();
    this._config = config;
    this._isPreview = isPreview;
    this._previewScale = previewScale;
    const { unitId, subUnitId } = this._config;
    this._key = `${unitId}_${subUnitId}_print`;
    this._jnpfPrintService = this._accessor.get(JnpfSheetsPrintService);
    this._univerInstanceService = this._accessor.get(IUniverInstanceService);
    this._skeleton = this._accessor
      .get(IRenderManagerService)
      .getRenderById(unitId)
      ?.with(SheetSkeletonManagerService)
      .getOrCreateSkeleton({ sheetId: subUnitId });
    this._sheetPrintInterceptorService = this._accessor.get(SheetPrintInterceptorService);
    this._initRenderer();
  }

  get key() {
    return this._key;
  }

  get engine() {
    return this._engine;
  }

  get container() {
    return this._container;
  }

  get _contentConfig() {
    return this._jnpfPrintService.contentConfig;
  }

  get _printConfig() {
    return this._jnpfPrintService.printConfig;
  }

  dispose() {
    super.dispose();
    if (this._container.parentElement) {
      this._container.parentElement.removeChild(this._container);
    }
  }

  get _paperSize() {
    return {
      w: this._jnpfPrintService.paperSize.w * this._previewScale,
      h: this._jnpfPrintService.paperSize.h * this._previewScale,
    };
  }

  get _margin() {
    const paperMargin = this._jnpfPrintService.paperMargin;
    return {
      top: paperMargin.top * this._previewScale,
      bottom: paperMargin.bottom * this._previewScale,
      left: paperMargin.left * this._previewScale,
      right: paperMargin.right * this._previewScale,
    };
  }

  _initRenderer() {
    const pageSize = this._paperSize;
    const scale = this._config.scale * this._previewScale;
    const devicePixelRatio = this._isPreview ? Math.max(1, window.devicePixelRatio) : 1;
    this._engine = new Engine(pageSize.w, pageSize.h, devicePixelRatio, CanvasRenderMode.Printing);
    this._scene = new Scene(this._key, this._engine);
    this._scene.scale(scale, scale);
    this._engine.setContainer(this._container, this._isPreview);
    this._addComponent();
    this._addViewport();
    this.disposeWithMe({
      dispose: () => {
        this._engine.dispose();
        this._scene.dispose();
      },
    });
  }

  setPreviewScale(scale) {
    this._scene.scale(this._config.scale * scale, this._config.scale * scale);
    this._previewScale = scale;
    this.markDirty(true);
  }

  updateConfig(config) {
    this._config = config;
    this.markDirty(true);
  }

  markDirty(dirty) {
    this._dirty = dirty;
  }

  _addComponent() {
    const spreadsheet = new Spreadsheet('__JnpfSheetPrintRender__', this._skeleton, false);
    spreadsheet.isPrinting = true;
    this._scene.addObject(spreadsheet);
    this._spreadsheetObject = spreadsheet;
    const { unitId, subUnitId } = this._config;
    this._sheetPrintInterceptorService.interceptor.fetchThroughInterceptors(
      this._sheetPrintInterceptorService.interceptor.getInterceptPoints().PRINTING_COMPONENT_COLLECT,
    )(undefined, { unitId, subUnitId, scene: this._scene });
  }

  _addViewport() {
    this._viewMain = new Viewport(VIEWPORT_KEY.VIEW_MAIN, this._scene);
    this._viewLeft = new Viewport(VIEWPORT_KEY.VIEW_MAIN_LEFT, this._scene);
    this._viewTop = new Viewport(VIEWPORT_KEY.VIEW_MAIN_TOP, this._scene);
    this._viewLeftTop = new Viewport(VIEWPORT_KEY.VIEW_MAIN_LEFT_TOP, this._scene);
  }

  _resizeViewport() {
    const { freeze, range } = this._config;
    const { xSplit, ySplit, startColumn, startRow } = freeze;
    const { rowHeaderWidth, columnHeaderHeight } = this._skeleton;
    this._scene.scale(this._config.scale * this._previewScale, this._config.scale * this._previewScale);
    const cellPositionSplit = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(startRow - ySplit, startColumn - xSplit);
    const cellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(startRow, startColumn);
    const startXSplit = xSplit > 0 ? cellPosition.startX - cellPositionSplit.startX : 0;
    const startYSplit = ySplit > 0 ? cellPosition.startY - cellPositionSplit.startY : 0;
    const _range = {
      startX: xSplit > 0 ? cellPositionSplit.startX : 0,
      endX: xSplit > 0 ? cellPosition.startX : 0,
      startY: ySplit > 0 ? cellPositionSplit.startY : 0,
      endY: ySplit > 0 ? cellPosition.startY : 0,
    };
    const startCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.startRow, range.startColumn);
    const precisionScale = this._scene.getPrecisionScale();
    const getScaleX = value => roundScale(value, precisionScale.scaleX);
    const getScaleY = value => roundScale(value, precisionScale.scaleY);
    const endCellPosition = this._skeleton.getNoMergeCellPositionByIndexWithNoHeader(range.endRow, range.endColumn),
      endX = Math.max(0, getScaleX(endCellPosition.endX) - Math.max(startCellPosition.startX, _range.endX)),
      endY = Math.max(0, getScaleY(endCellPosition.endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY))),
      scale = 1 / Math.max(precisionScale.scaleX, precisionScale.scaleY);
    this._totalWidth = startXSplit + endX + scale;
    this._totalHeight = startYSplit + endY + scale;
    const startSplit = { x: startXSplit, y: startYSplit };
    const scaleY = scale,
      scaleX = scale,
      scaleXY = {
        x: getScaleX(endCellPosition.endX) - getScaleX(endX) - getScaleX(Math.max(startCellPosition.startX, _range.endX)),
        y: getScaleY(endCellPosition.endY) - getScaleY(endY) - getScaleY(Math.max(startCellPosition.startY, _range.endY)),
      };
    if (xSplit > 0 && ySplit > 0) {
      this._viewLeftTop.enable();
      this._viewLeftTop.resizeWhenFreezeChange({
        top: 0,
        left: 0,
        height: getScaleY(startSplit.y),
        width: getScaleX(startSplit.x),
      });
      this._viewLeftTop.updateScrollVal({
        viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
        viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
      });
    } else {
      this._viewLeftTop.disable();
    }

    if (xSplit > 0) {
      this._viewLeft.enable();
      this._viewLeft.resizeWhenFreezeChange({
        top: getScaleX(startSplit.y),
        left: 0,
        height: getScaleY(endY) + scaleY + scaleXY.y,
        width: getScaleX(startSplit.x),
      });
      this._viewLeft.updateScrollVal({
        viewportScrollX: getScaleX(_range.startX) + getScaleX(rowHeaderWidth),
        viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
      });
    } else {
      this._viewLeft.disable();
    }
    if (ySplit > 0) {
      this._viewTop.enable();
      this._viewTop.resizeWhenFreezeChange({
        top: 0,
        left: getScaleX(startSplit.x),
        height: getScaleY(startSplit.y),
        width: getScaleX(endX) + scaleX + scaleXY.x,
      });
      this._viewTop.updateScrollVal({
        viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
        viewportScrollY: getScaleY(_range.startY) + getScaleY(columnHeaderHeight),
      });
    } else {
      this._viewTop.disable();
    }
    this._viewMain.resizeWhenFreezeChange({
      top: getScaleY(startSplit.y),
      left: getScaleX(startSplit.x),
      height: getScaleY(endY) + scaleY + scaleXY.y,
      width: getScaleX(endX) + scaleX + scaleXY.x,
    });
    this._viewMain.updateScrollVal({
      viewportScrollX: getScaleX(startCellPosition.startX) + getScaleX(rowHeaderWidth) - getScaleX(startSplit.x),
      viewportScrollY: getScaleY(startCellPosition.startY) + getScaleY(columnHeaderHeight) - getScaleY(startSplit.y),
    });
  }

  _getTranslateOffset() {
    const margin = this._margin;
    const { vAlign, hAlign } = this._contentConfig;
    const scale = Math.max(this._scene.scaleX, this._scene.scaleY);
    let offsetX;
    switch (hAlign) {
      case JnpfPaperAlign.Start:
        offsetX = margin.left;
        break;
      case JnpfPaperAlign.End:
        offsetX = this._paperSize.w - this._totalWidth * scale - margin.right;
        break;
      default:
        offsetX = margin.left + (this._paperSize.w - this._totalWidth * scale - margin.left - margin.right) / 2;
        break;
    }
    let offsetY;
    switch (vAlign) {
      case JnpfPaperAlign.Start:
        offsetY = margin.top;
        break;
      case JnpfPaperAlign.End:
        offsetY = this._paperSize.h - this._totalHeight * scale - margin.bottom;
        break;
      default:
        offsetY = (this._paperSize.h - this._totalHeight * scale - margin.top - margin.bottom) / 2 + margin.top;
        break;
    }
    return { offsetX: Math.round(offsetX), offsetY: Math.round(offsetY) };
  }

  prepare() {
    this._resizeViewport();
  }

  _renderMainScene() {
    this._dirty && this.prepare();
    const context = this._engine.getCanvas().getContext();
    const { gridlines } = this._contentConfig;
    const { offsetX, offsetY } = this._getTranslateOffset();
    this._spreadsheetObject.setForceDisableGridlines(!gridlines);
    this._spreadsheetObject.makeForceDirty();
    this._scene.makeDirty();
    context.save();
    context.translateWithPrecision(offsetX, offsetY);
    this._scene.render();
    context.restore();
  }

  _renderHeaderFooter() {
    const workbook = this._univerInstanceService.getUnit<Workbook>(this._config.unitId, UniverType.UNIVER_SHEET);
    if (!workbook) {
      return;
    }
    const worksheet = workbook.getSheetBySheetId(this._config.subUnitId);
    if (!worksheet) {
      return;
    }
    const { headerFooter } = this._contentConfig;
    const { w, h } = this._paperSize;
    const ctx = this._engine.getCanvas().getContext();
    ctx.save();
    ctx.font = `13px ${DEFAULT_FONTFACE_PLANE}`;
    const x = 19 * this._previewScale;
    const y = 20 * this._previewScale;
    if (headerFooter.includes(JnpfPrintPageInfo.Date) || headerFooter.includes(JnpfPrintPageInfo.Time)) {
      let dateStr = new Date().toLocaleString();
      headerFooter.includes(JnpfPrintPageInfo.Date)
        ? headerFooter.includes(JnpfPrintPageInfo.Time) || (dateStr = new Date().toLocaleDateString())
        : (dateStr = new Date().toLocaleTimeString());
      ctx.fillText(dateStr, x, h - y);
    }

    const { WorkbookTitleText } = this._printConfig;

    headerFooter.forEach(info => {
      switch (info) {
        case JnpfPrintPageInfo.WorkbookTitle: {
          const text = ctx.measureText(WorkbookTitleText);
          const length = text.fontBoundingBoxAscent + text.fontBoundingBoxDescent;
          ctx.fillText(WorkbookTitleText, x, y + length);
          break;
        }
        case JnpfPrintPageInfo.WorksheetTitle: {
          const name = worksheet.getName();
          const text = ctx.measureText(name);
          const length = text.fontBoundingBoxAscent + text.fontBoundingBoxDescent;
          ctx.fillText(name, w - (text.width + x), y + length);
          break;
        }
        case JnpfPrintPageInfo.PageSize: {
          const name = `${this._config.page}`;
          const text = ctx.measureText(name);
          ctx.fillText(name, w - (text.width + x), h - y);
          break;
        }
      }
    });
    ctx.restore();
  }

  _renderWaterMark() { }

  render() {
    this._engine.clearCanvas();
    this._renderMainScene();
    this._renderHeaderFooter();
    this._renderWaterMark();
  }

  renderOnReady() {
    const observable = this.engine.onTransformChange$.subscribeEvent(() => {
      this.render();
    });
    this.disposeWithMe(observable);
  }
}
